"""
Job definition example:

[meta]
priority=10
loglevel=0

[order]
calendar=all

[conditions]
gotest=0

[resources]
sas=1

[step01]
kind=sas
code=
	data xxx;
		set sashelp.class;
	run;

[step02]
kind=sas
code=
	data yyy;
		set sashelp.class;
	run;

"""

import cal
import cfg
import cnd
import log
import res
import step
import test
import util

from fnmatch import fnmatch
import os
from threading import Thread
import time
import re
from pprint import pprint,pformat
import shutil
import traceback

class Job:
	def __init__(self,name,date,signal_queue=None,fake=False):
		self.name = name
		self.date = date
		self.signal_queue = signal_queue
		self.fake = fake
		self.wait_for = [] # [(cnd,date)]
		self.resources = {} # res -> cnt
		self.time_windows = [] # [(start,stop)]
		self.date_min = None # sysdate
		self.date_max = None # sysdate
		self.steps = [] # [Step]
		self.tests = [] # [Test]
		self.cfg = {} # 'section':{'k':'v'}
		self.priority = 50 # low=best
		self.loglevel = log.INFO
		self.logdir = cfg.get_step_log_dir(date,name)
		self.outdir = self.get_out_path(remove=False)
		self.runid = self.get_next_runid()
		self.variables = {'job':name,'date':date}
		self.variables['yyyy'] = date[0:4]
		self.variables['mm'] = date[4:6]
		self.variables['dd'] = date[6:8]
		self.variables['outdir'] = self.outdir
		self.variables['outroot'] = cfg.out_root_dir
		self.variables['jobdir'] = cfg.job_root_dir
		self.variables['cfgdir'] = cfg.cfg_root_dir
		self.variables['cnddir'] = cnd.get_cnd_dir(date)
		self.variables['cndroot'] = cfg.cnd_root_dir
		self.variables['logdir'] = self.logdir # real logdir is created in self.run
		self.variables['runid'] = self.runid
		self.load_definition()
		self.log = log.get_job_logger(name,date,self.runid,self.loglevel)

	def __repr__(self):
		return "job-%s-%s" % (self.name,self.date)

	def load_definition(self):
		path = os.path.join(cfg.job_root_dir,self.name+'.ini')
		self.path = path
		for i in range(32):
			cp = cfg.Parser()
			#print "load_definition", path # xxx
			cp.read(path,**self.variables)
			for s in cp.sections():
				s = s.lower()
				d = {}
				for k,v in cp.items(s):
					d[k]=v
				if s.find('step')==0:
					if self.fake:
						self.steps += [(s,d)]
					else:
						self.steps += [step.load(s,d,self)]
				elif s.find('test')==0:
					self.tests += [test.load(s,d,self)]
				elif s.find('replica')==0:
					rjob=d.get('job','')
					rselect=d.get('select','')
					patterns = [x.strip() for x in rselect.split() if x.strip()]
					rcp=cfg.Parser()
					rpath=os.path.join(cfg.job_root_dir,rjob+'.ini')
					rcp.read(rpath,**self.variables)
					for s in rcp.sections():
						match = sum([int(fnmatch(s,p)) for p in patterns])
						if match:
							s = s.lower()
							d = {}
							for k,v in rcp.items(s):
								d[k]=v
							# code copied from above
							if s.find('step')==0:
								if self.fake:
									self.steps += [(s,d)]
								else:
									self.steps += [step.load(s,d,self)]
							elif s.find('test')==0:
								self.tests += [test.load(s,d,self)]
							else:
								self.cfg[s]=d
				else:
					self.cfg[s]=d
			if 'template' in cp.sections():
				for k,v in cp.items('template'):
					self.variables[k]=v.replace("\n","\n\t")
				path = os.path.join(cfg.job_root_dir,'template',self.variables['template']+'.ini')
			else:
				break
		if self.fake:
			self.steps.sort()
		else:
			self.steps.sort(key=lambda s:s.name)
		self.load_definition_stage2()

	def load_definition_stage2(self):
		meta = self.cfg.get('meta',{})
		self.priority = meta.get('priority',self.priority)
		self.loglevel = int(meta.get('loglevel',self.loglevel))
		self.tags = set([t.lower() for t in re.split("\s+",meta.get('tags','')) if t])
		self.hint = meta.get('hint','')
		self.auto_unlock = int(meta.get('auto_unlock','1'))
		self.max_active = int(meta.get('max_active','1'))
		for k,v in self.cfg.get("conditions",{}).items():
			self.wait_for += [(k,cal.offset(self.date,v))]
		for k,v in self.cfg.get("resources",{}).items():
			self.resources[k] = max(0,int(v.strip()))
		date_min = meta.get('date_min','').strip()
		date_max = meta.get('date_max','').strip()
		if date_min:
			self.date_min = cal.offset(self.date,date_min)
		if date_max:
			self.date_max = cal.offset(self.date,date_max)
		time_win = meta.get('time_windows',meta.get('time_window','')).split()
		self.time_windows = [tuple(w.split('-')+['24:00'])[:2] for w in time_win]
		self.cycle = int(meta.get('cycle','0'))
		self.retry = int(meta.get('retry','0'))
		self.cont = int(meta.get('continue','0'))
		order = self.cfg.get('order',{})
		self.order_cal = order.get('calendar','')
		self.order_months = [int(x) for x in order.get('months','').split()]
		self.order_daysofweek = [int(x) for x in order.get('daysofweek','').split()]
		self.order_daysofmonth = [int(x) for x in order.get('daysofmonth','').split()]
		self.order_caldaysofmonth = [int(x) for x in order.get('caldaysofmonth','').split()]
		self.order_formulas = [x.strip() for x in order.get('formulas','').split("\n") if x.strip()]
		self.order_days = order.get('days','').split()
		if not self.fake:
			self.save_wait_for()

	def send_signal(self):
		if self.signal_queue:
			self.signal_queue.put((self.name,self.date),True)
		else:
			raise Exception("job has no signal queue")

	def steps_ok(self):
		p = self.get_out_path(remove=False)
		return set([s.split('.')[0] for s in os.listdir(p) if re.findall("step[0-9]+[.]ok",s)])

	def waiting_for(self):
		return [(c,d) for c,d in self.wait_for if not cnd.exists(c,d)]

	def end_should_run(self):
		end_should_run = not (cnd.exists(self.name+'.ok',self.date) or cnd.exists(self.name+'.error',self.date))
		return end_should_run

	def cnd_should_run(self):
		#print self.name, "waits for", self.waiting_for() # xxx
		if cnd.exists(self.name+'.stop',self.date) or cnd.exists('all.stop',self.date):
			return False
		if cnd.exists(self.name+'.cnd.force',self.date) or cnd.exists(self.name+'.force',self.date):
			return True
		return len(self.waiting_for())==0

	def res_should_run(self):
		for r,cnt in self.resources.items():
			if not res.available(r,cnt):
				return False
		return True

	def time_should_run(self):
		if not self.time_windows:
			return True
		if cnd.exists(self.name+'.time.force',self.date) or cnd.exists(self.name+'.force',self.date):
			return True
		for start,stop in self.time_windows:
			t = time.strftime("%H:%M")
			if start <= t <= stop:
				return True
		return False

	def test_should_run(self):
		#print "test_should_run"
		if cnd.exists(self.name+'.test.force',self.date):
			return True
		for t in self.tests:
			if not t.run():
				return False
		return True

	def date_should_run(self):
		d = time.strftime("%Y%m%d")
		if cnd.exists(self.name+'.date.force',self.date) or cnd.exists(self.name+'.force',self.date):
			return True
		if self.date_min:
			if d < self.date_min:
				return False
		if self.date_max:
			if d > self.date_max:
				return False
		return True

	def max_active_should_run(self):
		if cnd.exists(self.name+'.max_active.force',self.date):
			return True
		active_cnt = len(cnd.active_cnt(self.name))
		return active_cnt < self.max_active

	def should_run(self):
		return self.end_should_run() and self.time_should_run() and self.date_should_run() \
		   and self.cnd_should_run() and self.res_should_run() and self.test_should_run() and self.max_active_should_run()

	def should_stop(self,step_name):
		step_stop = cnd.exists(self.name+'.'+step_name+'.stop',self.date)
		job_stop = cnd.exists(self.name+'.stop',self.date)
		all_stop = cnd.exists('all.stop',self.date)
		return all_stop or job_stop or step_stop

	def is_running(self):
		run_cnd = cnd.exists(self.name+'.run',self.date)
		end_cnd = cnd.exists(self.name+'.ok',self.date) or cnd.exists(self.name+'.error',self.date)
		return run_cnd and not end_cnd

	def is_ok(self):
		return cnd.exists(self.name+'.ok',self.date)

	def run(self,continue_=False):
		if self.is_running():
			return
		cnd.add(self.name+'.run',self.date)
		self.log.debug("Job.run")
		for r,cnt in self.resources.items():
			res.lock(r,self.name,self.date,cnt)
		self.create_log_dir()
		Thread(target=self.run_steps,args=[continue_]).start()

	def stop(self):
		cnd.add(self.name+'.stop',self.date)

	def should_be_ordered(self,date):
		if self.order_days:
			if date in self.order_days:
				return True
		d = cal.to_date(date)
		if self.order_months:
			if d.month not in self.order_months:
				return False
		if self.order_cal:
			c = cal.Calendar(self.order_cal)
			if date not in c.days:
				return False
			if not (self.order_months or self.order_daysofweek or self.order_daysofmonth or self.order_caldaysofmonth or self.order_formulas or self.order_days):
				return True				
			if self.order_caldaysofmonth:
				job_cdom = self.order_caldaysofmonth
				cal_days = c.daysofmonth(date)
				job_days = [cal_days[i if i<0 else i+1] for i in job_cdom if -len(cal_days)<=i<=len(cal_days)]
				if date in job_days:
					return True
		if self.order_daysofmonth:
			# TODO - negative day of month
			if d.day in self.order_daysofmonth:
				return True
		if self.order_daysofweek:
			if d.weekday()+1 in self.order_daysofweek:
				return True
		for f in self.order_formulas:
			if cal.offset(date,f)==date:
				return True
		return False

	def run_steps(self,continue_=False):
		self.log.info("job started")
		if cnd.exists(self.name+'.continue',self.date) or continue_:
			continue_=True
		else:
			continue_=False
		self.log_job_config()
		for step in self.steps:
			exc='step RC<>0'
			if self.should_stop(step.name):
				cnd.add(self.name+'.error',self.date)
				if self.auto_unlock and self.resources:
					cnd.add(self.name+'.unlock',self.date)
				return
			if continue_ and step.name in self.steps_ok():
				continue
			if cnd.exists(self.name+'.'+step.name+'.skip',self.date):
				self.log.debug("skip %s",step.name)
				# rc = 1 # WTF???
			else:
				self.log.debug("run %s",step.name)
				try:
					rc = step.run()
				except Exception,e:
					rc = 666
					exc="|".join(traceback.format_exc().split("\n"))
				self.variables[step.name+'.rc'] = str(rc)
				self.save_variables()
			try:
				shutil.copyfile(step.log_path,self.get_out_path(step.name+'.log'))
			except Exception,exc:
				self.log.error('cannot copy %s log to output dir -> %s',step.name,exc)
			rcok = step.cfg_get('rc_ok','0')
			rcvar = step.cfg_get('rc_var','').lower()
			nook = int(step.cfg_get('no_ok','0'))
			if rcvar:
				self.variables[rcvar]=str(rc)
				self.log.info("MACRO VARIABLE %s SET TO '%s'",rcvar,rc)
			if re.findall('^(?:'+rcok+')$',str(rc)):
				if nook:
					self.log.debug("%s ok but no_ok<>0",step.name)
				else:
					p = self.get_out_path(step.name+'.ok') # TODO - only if continuable???
					open(p,'w')
					self.log.debug("%s ok",step.name)
			elif rc<0:
				cnd.add(self.name+'.sleep',self.date)
				self.log.info('sleep rc=%s in %s kind=%s -> %s',rc,step.name,step.kind)
				return
			else:
				cnd.add(self.name+'.error',self.date)
				if self.auto_unlock and self.resources:
					cnd.add(self.name+'.unlock',self.date)
				self.log.error('rc=%s in %s kind=%s -> %s',rc,step.name,step.kind,exc)
				return
		cnd.add(self.name+'.ok',self.date)
		for r,cnt in self.resources.items():
			res.unlock(r,self.name,self.date)
		self.log.info("job ended")
		self.send_signal()

	def get_out_path(self,filename='',remove=True):
		return cfg.get_out_path(self.date,self.name,filename,remove)

	def get_next_runid(self):
		runid=0
		logdir = cfg.get_step_log_dir(self.date,self.name)
		for f in os.listdir(logdir):
			m = re.findall("^run(\d+)$",f)
			if m:
				runid=max(runid,int(m[0]))
		return runid+1
	
	def get_curr_runid(self):
		return self.get_next_runid()-1

	def get_step_log_path(self,stepname):
		import os
		p = os.path.join(self.logdir,"%(stepname)s.log"%locals())
		if not os.path.exists(p):
			open(p,"w")
		return p

	def create_log_dir(self):
		import os
		d = os.path.join(self.logdir,"run"+str(self.runid))
		if not os.path.exists(d):
			os.makedirs(d)
		self.logdir=d
		self.variables['logdir'] = self.logdir

	def log_job_config(self):
		p = self.get_step_log_path('config')
		f = open(p,'w')
		for step in self.steps:
			rec = "%s class=%s %s" % (step.name,step.__class__.__name__,pformat(step.__dict__.items()))
			f.write(rec+"\n\n\n")

	def past_flow(self,dist_max=1):
		"returns [distance]->set([[jobname,date]...])"
		out = {1:set()}
		for cnd,date in self.wait_for:
			jobname=cnd.split('.')[0]
			out[1].add((jobname,date))
			if dist_max>1:
				job=Job(jobname,date,fake=True)
				for dist,jd_set in job.past_flow(dist_max-1).items():
					if not dist+1 in out:
						out[dist+1]=set()
					out[dist+1].update(jd_set)
		return out

	def future_flow(self,dist_max=1):
		"returns [distance]->set([[jobname,date]...])"
		out = {1:set()}
		waits_for_job=[]
		d = os.path.join(cfg.cnd_root_dir,self.date,'wait_for',self.name)
		if os.path.exists(d):
			for x in os.listdir(d):
				waits_for_job+=[x.split('.')]
		for jobname,date in waits_for_job:
			out[1].add((jobname,date))
			if dist_max>1:
				job=Job(jobname,date,fake=True)
				for dist,jd_set in job.future_flow(dist_max-1).items():
					if not dist+1 in out:
						out[dist+1]=set()
					out[dist+1].update(jd_set)
					if not out[dist+1]:
						del out[dist+1]
		return out

	def save_wait_for(self):
		for cnd,date in self.wait_for:
			jobname=cnd.split('.')[0]
			d = os.path.join(cfg.cnd_root_dir,date,'wait_for',jobname)
			if not os.path.exists(d):
				os.makedirs(d)
			fp=os.path.join(d,self.name+'.'+self.date)
			try:
				open(fp,'w')
			except:
				pass
	
	def save_variables(self):
		cp=cfg.Parser()
		cp.add_section('variables')
		for k,v in self.variables.items():
			cp.set('variables',k,v)
		p = self.get_out_path('variables.ini')
		try:
			cp.write(open(p,'w'))
		except Exception,e:
			self.log.error('cannot write variables.ini file')
	
	def get_stored_variables(self):
		p = self.get_out_path('variables.ini',remove=False)
		if not os.path.exists(p):
			return
		cp=cfg.Parser()
		cp.read(p)
		var_dict = dict(cp.items('variables'))
		return var_dict

def all_job_names():
	return [j.lower()[:-4] for j in os.listdir(cfg.job_root_dir) if j.lower().endswith('.ini')]

if __name__=="__main__":
	#from pprint import pprint
	#cnd.clear_all('20111025')
	cfg.job_root_dir=r"Y:\riv_prod\job"
	j=Job("xxx_task_c","20111025",None)
	#j.run()
	#~ cnd.clear_all('20111025')
	#~ j=Job("test","20111025",None)
	#~ print "should_be_ordered", j.should_be_ordered("20111025")
	#~ print j.waiting_for()
	#~ print j.should_run()
	#~ print "end",j.end_should_run()
	#~ print "time",j.time_should_run()
	#~ print "cnd",j.cnd_should_run()
	#~ print "test",j.test_should_run()
	#~ print "res",j.res_should_run()
	#~ pprint(j.cfg)
	#~ pprint(j.__dict__)
	#~ c = cal.Calendar('all')
	#~ print "all_job_names", all_job_names()
	#~ print "should_run",j.should_run()
	#~ if j.should_run():
		#~ j.run(False)
	#for d in c.days:
	#	print d,j.should_be_ordered(d)
